# -*- encoding: utf-8 -*-
#
# This piece of code is a sqlalchemy abstraction to store setting-name and
# setting-value pairs in a database. Copyright (C) 2009  Dennis Keitzel
# 
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# Version 0.10
#

# Choose your desired tablename:
_SETTINGS_TABLE_NAME = 'settings'

from UserDict import DictMixin
from sqlalchemy import Column, Integer, Text, Table, Float, Boolean
from database import Database

db = Database()

class _Setting(db.Base):
	'''Represents a setting-name and setting-value pair in the database.
	Do NOT use this directly as it gets wrapped inside the dictionary like class
	'Settings'.
	'''
	__tablename__ = _SETTINGS_TABLE_NAME
	setting = Column(Text, unique=True, primary_key=True)
	value_str = Column(Text)
	value_int = Column(Integer)
	value_float = Column(Float)
	value_bool = Column(Boolean)

	def __init__(self, setting, value):
		'''instantiate an object of this class.
		
		@param setting: the name of the setting to be stored
		@type setting: string
		@param value: the value of the setting to be stored
		@type value: str, unicode, int, float, bool or None
		'''
		if not isinstance(setting, basestring):
			raise TypeError('setting has to be a string')
		self.setting = setting.lower()
		self.value = value # Notice: self.value is a property!
		
	def _set_value(self, value):
		'''Store the value accordingly to it's type, if supported.'''
		self.value_str = None
		self.value_int = None
		self.value_float = None
		if isinstance(value, basestring):
			self.value_str = value
		elif isinstance(value, int):
			if type(value) is bool:
				self.value_bool = value
			else:
				self.value_int = value
		elif isinstance(value, float):
			self.value_float = value
		elif value is None:
			pass # leave everything to None
		else:
			msg = "Unsupported type '%s' for value. Supported types: %s" \
				% (repr(type(value)), 'str, unicode, int, float, bool, None')
			raise TypeError(msg)
		
	def _get_value(self):
		'''Return the right value.'''
		return max(self.value_str, self.value_int, self.value_float, self.value_bool)
		
	value = property(fget=_get_value, fset=_set_value)

	def __repr__(self):
		'''Return a string representation.'''
		return self.setting
	
	def __eq__(self, obj):
		'''Return if the given object is equal to self.'''
		if isinstance(obj, _Setting):
			return self.setting == obj.setting \
				and self.value_float == obj.value_float \
				and self.value_int == obj.value_int \
				and self.value_str == obj.value_str \
				and self.value_bool == obj.value_bool
		else:
			return False
		
class Settings(DictMixin):
	'''Dictionary-like class which maintains the data in a database table.
	Dictionary-like means, that you can use this as it would be a real
	dictionary.

	Example:
		s = Settings()
		s['test-setting'] = 'foobar'
		'foobar' in s --> True
		s['test-setting'] --> 'foobar'
		len(s) --> 1
		for key in s: print key --> 'test-setting'
		s.keys() --> ['test-setting']
		and so on..
	
	Limitations:
		Keys must be of type string or unicode.
		Values must be of type str, unicode, int, float or None.
	'''
	def __init__(self, dict=None, **kwargs):
		'''Instantiate an object of this class.
		
		Settings() -> new empty settings-dictionary.
		Settings(mapping) -> new settings-dictionary initialized from 
			mapping object's (key, value) pairs.
	    	Settings(seq) -> new settings-dictionary initialized as if via:
			d = Settings()
			for k, v in seq:
				d[k] = v
		Settings(**kwargs) -> new settings-dictionary initialized with the
			name=value pairs in the keyword argument list. For example:
			Settings(one=1, two=2)
		'''
		# Create table if nonexistent
		if not db.engine.has_table(_Setting.__tablename__):
			db.engine.create(_Setting.metadata)

		if dict is not None:
			self.update(dict)
		if len(kwargs):
			self.update(kwargs)
		
	def __check_key(self, key):
		'''Check if the given key is an instance of basestring.'''
		if not isinstance(key, basestring):
			raise TypeError('key has to be a string')
	
	def __len__(self):
		'''Return the number of settings currently stored in the database.'''
		return int(db.Session.query(_Setting).count())
	
	def __getitem__(self, key):
		'''Return the value associated with the given key, if available.'''
		self.__check_key(key)
		key = key.lower()
		setting = db.Session.query(_Setting).filter_by(setting=key).first()
		if setting:
			return setting.value
		else:
			raise KeyError(key)
	
	def __setitem__(self, key, value):
		'''Set the given value for the given key and store this pair in the
		database. If the key already exists, the associated value will be
		overwritten with the new one.'''
		self.__check_key(key)
		key = key.lower()
		setting = db.Session.query(_Setting).filter_by(setting=key).first()
		if setting:
			setting.value = value
		else:
			setting = _Setting(key, value)
			db.Session.add(setting)
		db.Session.commit()
		
	def __delitem__(self, key):
		'''If the key is available in the database, it will be deleted.'''
		self.__check_key(key)
		key = key.lower()
		setting = db.Session.query(_Setting).filter_by(setting=key).first()
		if setting:
			db.Session.delete(setting)
			db.Session.commit()
		else:
			raise KeyError(key)
	
	def __contains__(self, key):
		'''Return whether the given key exists in the database.'''
		self.__check_key(key)
		key = key.lower()
		if db.Session.query(_Setting.setting).filter_by(setting=key).first():
			return True
		else:
			return False
	
	def __iter__(self):
		'''Return an iterator which iterates through all available keys.'''
		settings = db.Session.query(_Setting).order_by(_Setting.setting).all()
		for setting in settings:
			yield setting.setting
	
	def keys(self):
		'''Return a list of all available keys.'''
		settings = db.Session.query(_Setting).order_by(_Setting.setting).all()
		return [setting.setting for setting in settings]
	
	def iteritems(self):
		'''Return an iterator which iterates through all setting-name and
		setting-value pairs as tuples.'''
		settings = db.Session.query(_Setting).order_by(_Setting.setting).all()
		for setting in settings:
			yield (setting.setting, setting.value)
			
	def has_key(self, key):
		'''Return whether the given key exists in the database.'''
		return key in self
	